package ui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Properties;
import java.util.Vector;

import javax.swing.JPanel;

import org.apache.log4j.Logger;
import org.dyn4j.dynamics.Body;
import org.dyn4j.dynamics.BodyFixture;
import org.dyn4j.dynamics.World;
import org.dyn4j.geometry.Mass;
import org.dyn4j.geometry.Rectangle;

import utility.AppUtilities;

/**
 * 
 * @author manh.vu
 * 
 */
public abstract class AbsAnimationPanel extends JPanel implements AnimationLoopIfc, Runnable {
	private static final Logger LOGGER = Logger.getLogger(AbsAnimationPanel.class);
	/**
	 * 
	 */
	private static final long serialVersionUID = 551989222159496890L;
	private long PERIOD;
	private static final int NO_DELAYS_PER_YIELD = 16;
	private static final int MAX_FRAME_SKIPS = 5;
	private long beforeTime;
	private long timeDiff;
	private long afterTime;
	private long sleepTime;
	private Thread animator; // for the animation
	private volatile boolean running; // stops the animation
	private volatile boolean gameOver; // for termination

	protected final AppUtilities utilities;
	protected Properties prop; // hold properties file

	// global variables for off-screen rendering
	private Graphics2D dbG;
	private Image dbImage; // double buffer
	private volatile Vector<GameElementIfc> gameObjects; // holding object of game for drawing

	/**
	 * Next is DYN4J
	 */
	private World world;
	private Body body;

	protected AbsAnimationPanel() {
		this.utilities = AppUtilities.createAppUtilities();
		this.prop = utilities.openProperties(utilities.openConfigurationFile("ui.properties"));
		this.PERIOD = 1000L / 80;
		this.gameObjects = new Vector<>();

		try {
			utilities.put(getClass().getName(), this);
		}catch (Exception e) {
			LOGGER.warn(e.getMessage(), e.getCause());
		}
	}

	public void initialize() {
		this.running = false;
		this.gameOver = false;

		setLayout(new GridLayout());
		setBackground(Color.DARK_GRAY);
		setFocusable(true);
		requestFocus(); // JPanel now receives key events
		readyForTermination();

		// Init dyn4j
		this.world = new World();
		this.body = new Body();

		BodyFixture fixture = new BodyFixture(new Rectangle(15.0, 1.0));
		body.addFixture(fixture);
		body.setMass(Mass.Type.INFINITE);
		// move the floor down a bit
		body.translate(0.0, -4.0);
		world.addBody(body);
	}

	private void readyForTermination() {
		addKeyListener(new KeyAdapter() {
			// listen for esc, q, end, ctrl-c
			public void keyPressed(KeyEvent e) {
				int keyCode = e.getKeyCode();
				if((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) || (keyCode == KeyEvent.VK_END) || ((keyCode == KeyEvent.VK_C) && e.isControlDown())) {
					stop();
				}
			}
		});
	} // end of readyForTermination( )

	@Override
	public void addNotify() {
		initialize();
		// Start game
		start();
		super.addNotify(); // creates the peer
	}

	// Initialize and start the thread
	public void start() {
		if(!running) {
			if(this.getWidth() <= 0 || this.getHeight() <= 0) {
				LOGGER.warn("Size of panel is not initialized correctly [" + this.getWidth() + " x " + this.getHeight() + "]");
			}
			else if(animator == null) {
				this.animator = new Thread(this);
				this.animator.start();
			}
		}
	} // end of start( )

	// called by the user to stop execution
	public void stop() {
		running = false;
	}

	@Override
	public void update(double deltaT) {
		for(int i = 0; i < getNumberOfObjects(); i++) {
			this.gameObjects.get(i).doMove(deltaT);
		}
	}

	// draw the current frame to an image buffer
	@Override
	public synchronized void render() {
		Dimension size = this.getSize();

		if(dbImage == null) { // create the buffer
			dbImage = this.createImage(size.width, size.height);
			if(dbImage == null) {
				LOGGER.warn("dbImage is null, check system property \"java.awt.headless\"");
				return;
			}
			else
				dbG = (Graphics2D) dbImage.getGraphics();
		}

		// draw elements
		for(int i = 0; i < getNumberOfObjects(); i++) {
			this.gameObjects.get(i).drawBody(dbG);
		}

		// ...
		if(gameOver) {
			gameOverMessage(dbG);
		}
	} // end of Render( )

	// center the -over message
	protected abstract void gameOverMessage(Graphics2D g);

	@Override
	public abstract void sleep();

	/* Repeatedly update, render, sleep */
	@Override
	public void run() {
		running = true;
		long overSleepTime = 0L;
		int noDelays = 0;
		long excess = 0L;
		int skips = 0;
		beforeTime = System.nanoTime();
		while (running) {
			if(!gameOver) {
				if(sleepTime < 0) {
					sleepTime = timeDiff;
				}
				update(sleepTime); // state is updated
			}
			render(); // render to a buffer

			// repaint(); // paint with the buffer
			paintScreen();

			afterTime = System.nanoTime();
			timeDiff = afterTime - beforeTime;
			sleepTime = (PERIOD - timeDiff) - overSleepTime; // time left in this loop

			// update/render took longer than period
			if(sleepTime > 0) { // some time left in this cycle
				try {
					Thread.sleep(sleepTime / 1000000L); // nano -> ms
				}catch (InterruptedException ex) {
					LOGGER.error(ex.getMessage(), ex.getCause());
				}
				overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
			}
			else { // sleepTime <= 0; frame took longer than the period
				excess -= sleepTime; // store excess time value
				overSleepTime = 0L;
				if(++noDelays >= NO_DELAYS_PER_YIELD) {
					Thread.yield(); // give another thread a chance to run
					noDelays = 0;
				}
				skips = 0;
			}

			/*
			 * If frame animation is taking too long, update the game state without rendering it, to get the updates/sec nearer to the required FPS.
			 */
			while ((excess > PERIOD) && (skips < MAX_FRAME_SKIPS)) {
				beforeTime = System.nanoTime();
				excess -= PERIOD;
				if(!gameOver) {
					if(sleepTime < 0) {
						sleepTime = timeDiff;
					}
					update(sleepTime); // update state but don't render
				}
				skips++;
				sleepTime = System.nanoTime() - beforeTime;
			}

			beforeTime = System.nanoTime();
		}
	}

	/*
	 * @Override public void paintComponents(Graphics g) { super.paintComponents(g); if(dbImage != null) g.drawImage(dbImage, 0, 0, null); }
	 */

	// actively render the buffer image to the screen
	private void paintScreen() {
		Graphics g;
		try {
			g = this.getGraphics(); // get the panel's graphic context
			if((g != null) && (dbImage != null))
				g.drawImage(dbImage, 0, 0, null);
			Toolkit.getDefaultToolkit().sync(); // sync the display on some systems
			g.dispose();

		}catch (Exception e) {
			LOGGER.error(e.getMessage(), e.getCause());
		}
	} // end of paintScreen( )

	public void addGameObjects(GameElementIfc gameObjects) {
		this.gameObjects.add(gameObjects);
	}

	public void removeGameObjects(int index) {
		if(index == 0) {
			LOGGER.error("Trying to remove background.");
			return;
		}
		this.gameObjects.remove(index);
	}

	public int getNumberOfObjects() {
		return this.gameObjects.size();
	}

	protected void setFPS(int fps) {
		if(fps < 40 || fps > 400) {
			LOGGER.warn("FPS must in range of (40, 400)");
			return;
		}
		this.PERIOD = 1000L / fps;
	}
}
